Türkçe

TypeScript jenerikleri üzerine kapsamlı bir rehber; sözdizimi, faydaları, ileri düzey kullanımı ve küresel yazılım geliştirmede karmaşık veri tiplerini ele alma.

TypeScript Generics: Sağlam Uygulamalar için Karmaşık Veri Tiplerinde Uzmanlaşma

JavaScript'in bir üst kümesi olan TypeScript, geliştiricilere statik tipleme yoluyla daha sağlam ve sürdürülebilir kod yazma gücü verir. En güçlü özelliklerinden biri olan jenerikler (generics), tür güvenliğini korurken çeşitli veri tipleriyle çalışabilen kodlar yazmanıza olanak tanır. Bu rehber, TypeScript jeneriklerini küresel yazılım geliştirme bağlamında karmaşık veri tiplerine uygulanmasına odaklanarak kapsamlı bir şekilde incelemektedir.

Jenerikler (Generics) Nedir?

Jenerikler, farklı türlerle çalışabilen yeniden kullanılabilir kod yazmanın bir yolunu sunar. Desteklemek istediğiniz her tür için ayrı fonksiyonlar veya sınıflar yazmak yerine, tür parametreleri kullanan tek bir fonksiyon veya sınıf yazabilirsiniz. Bu tür parametreleri, fonksiyon veya sınıf çağrıldığında veya örneklendiğinde kullanılacak olan gerçek türler için birer yer tutucudur. Bu, özellikle bu yapıların içindeki veri türünün değişebileceği karmaşık veri yapılarıyla uğraşırken kullanışlıdır.

Jenerik Kullanmanın Faydaları

Temel Jenerik Sözdizimi

Temel jenerik sözdizimi, tür parametrelerini bildirmek için açılı parantezlerin (< >) kullanılmasını içerir. Bu tür parametreleri genellikle T, K, V vb. olarak adlandırılır, ancak herhangi bir geçerli tanımlayıcıyı kullanabilirsiniz. İşte basit bir jenerik fonksiyon örneği:


function identity<T>(arg: T): T {
  return arg;
}

let myString: string = identity<string>("hello");
let myNumber: number = identity<number>(123);
let myBoolean: boolean = identity<boolean>(true);

console.log(myString); // Çıktı: hello
console.log(myNumber); // Çıktı: 123
console.log(myBoolean); // Çıktı: true

Bu örnekte, <T>, T adında bir tür parametresi bildirir. identity fonksiyonu, T türünde bir argüman alır ve T türünde bir değer döndürür. Fonksiyonu çağırırken, tür parametresini açıkça belirtebilir (örneğin, identity<string>) veya TypeScript'in argüman türüne göre çıkarım yapmasına izin verebilirsiniz.

Karmaşık Veri Tipleriyle Çalışma

Jenerikler, diziler, nesneler ve arayüzler gibi karmaşık veri tipleriyle uğraşırken özellikle değerli hale gelir. Bazı yaygın senaryoları inceleyelim:

Jenerik Diziler

Farklı türlerdeki dizilerle çalışan fonksiyonlar veya sınıflar oluşturmak için jenerikleri kullanabilirsiniz:


function arrayToString<T>(arr: T[]): string {
  return arr.join(", ");
}

let numberArray: number[] = [1, 2, 3, 4, 5];
let stringArray: string[] = ["apple", "banana", "cherry"];

console.log(arrayToString(numberArray)); // Çıktı: 1, 2, 3, 4, 5
console.log(arrayToString(stringArray)); // Çıktı: apple, banana, cherry

Burada, arrayToString fonksiyonu T[] türünde bir dizi alır ve dizinin bir dize temsilini döndürür. Bu fonksiyon her türden diziyle çalışır, bu da onu oldukça yeniden kullanılabilir kılar.

Jenerik Nesneler

Jenerikler ayrıca farklı şekillerdeki nesnelerle çalışan fonksiyonları veya sınıfları tanımlamak için de kullanılabilir:


interface Person {
  name: string;
  age: number;
  country: string; // Küresel bağlam için ülke eklendi
}

interface Product {
  id: number;
  name: string;
  price: number;
  currency: string; // Küresel bağlam için para birimi eklendi
}

function displayInfo<T extends { name: string }>(item: T): void {
  console.log(`Name: ${item.name}`);
}

let person: Person = { name: "Alice", age: 30, country: "USA" };
let product: Product = { id: 1, name: "Laptop", price: 1200, currency: "USD" };

displayInfo(person); // Çıktı: Name: Alice
displayInfo(product); // Çıktı: Name: Laptop

Bu örnekte, displayInfo fonksiyonu, string türünde bir name özelliğine sahip olması gereken T türünde bir nesne alır. extends { name: string } ifadesi, T tür parametresi için minimum gereksinimleri belirten bir kısıtlamadır (constraint). Bu, fonksiyonun name özelliğine güvenli bir şekilde erişebilmesini sağlar.

İleri Düzey Jenerik Kullanımı

TypeScript jenerikleri, daha da esnek ve güçlü kod oluşturmanıza olanak tanıyan daha gelişmiş özellikler sunar. Bu özelliklerden bazılarını inceleyelim:

Çoklu Tür Parametreleri

Birden çok tür parametresine sahip fonksiyonlar veya sınıflar tanımlayabilirsiniz:


function merge<T, U>(obj1: T, obj2: U): T & U {
  return { ...obj1, ...obj2 };
}

interface Name {
  firstName: string;
}

interface Age {
  age: number;
}

const person: Name = { firstName: "Bob" };
const details: Age = { age: 42 };

const merged = merge(person, details);
console.log(merged.firstName); // Çıktı: Bob
console.log(merged.age); // Çıktı: 42

merge fonksiyonu, T ve U türlerinde iki nesne alır ve her iki nesnenin özelliklerini içeren yeni bir nesne döndürür. Bu, farklı kaynaklardan gelen verileri birleştirmenin güçlü bir yoludur.

Jenerik Kısıtlamalar

Daha önce gösterildiği gibi, kısıtlamalar bir jenerik tür parametresiyle kullanılabilecek türleri sınırlamanıza olanak tanır. Bu, jenerik kodun belirtilen türler üzerinde güvenli bir şekilde çalışabilmesini sağlar.


interface Lengthwise {
  length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}

loggingIdentity([1, 2, 3]); // Çıktı: 3
loggingIdentity("hello"); // Çıktı: 5
// loggingIdentity(123); // Hata: 'number' türündeki argüman, 'Lengthwise' türündeki parametreye atanamaz.

loggingIdentity fonksiyonu, number türünde bir length özelliğine sahip olması gereken T türünde bir argüman alır. Bu, fonksiyonun length özelliğine güvenli bir şekilde erişebilmesini sağlar.

Jenerik Sınıflar

Jenerikler sınıflarla da kullanılabilir:


class DataStorage<T> {
  private data: T[] = [];

  addItem(item: T) {
    this.data.push(item);
  }

  removeItem(item: T) {
    this.data = this.data.filter(d => d !== item);
  }

  getItems(): T[] {
    return [...this.data];
  }
}

const textStorage = new DataStorage<string>();
textStorage.addItem("apple");
textStorage.addItem("banana");
textStorage.removeItem("apple");
console.log(textStorage.getItems()); // Çıktı: [ 'banana' ]

const numberStorage = new DataStorage<number>();
numberStorage.addItem(1);
numberStorage.addItem(2);
numberStorage.removeItem(1);
console.log(numberStorage.getItems()); // Çıktı: [ 2 ]

DataStorage sınıfı, herhangi bir T türünde veri depolayabilir. Bu, tür güvenliğine sahip yeniden kullanılabilir veri yapıları oluşturmanıza olanak tanır.

Jenerik Arayüzler

Jenerik arayüzler, farklı türlerle çalışabilen sözleşmeler tanımlamak için kullanışlıdır. Örneğin:


interface Result<T, E> {
  success: boolean;
  data?: T;
  error?: E;
}

interface User {
  id: number;
  username: string;
  email: string;
}

interface ErrorMessage {
  code: number;
  message: string;
}

function fetchUser(id: number): Result<User, ErrorMessage> {
  if (id === 1) {
    return { success: true, data: { id: 1, username: "john.doe", email: "john.doe@example.com" } };
  } else {
    return { success: false, error: { code: 404, message: "Kullanıcı bulunamadı" } };
  }
}

const userResult = fetchUser(1);
if (userResult.success) {
  console.log(userResult.data.username);
} else {
  console.log(userResult.error.message);
}

Result arayüzü, bir işlemin sonucunu temsil etmek için jenerik bir yapı tanımlar. Ya T türünde veri ya da E türünde bir hata içerebilir. Bu, asenkron işlemleri veya başarısız olabilecek işlemleri yönetmek için yaygın bir kalıptır.

Yardımcı Türler ve Jenerikler

TypeScript, jeneriklerle iyi çalışan birkaç yerleşik yardımcı tür sağlar. Bu yardımcı türler, türleri güçlü yollarla dönüştürmenize ve değiştirmenize yardımcı olabilir.

Partial<T>

Partial<T>, T türünün tüm özelliklerini isteğe bağlı yapar:


interface Person {
  name: string;
  age: number;
}

type PartialPerson = Partial<Person>;

const partialPerson: PartialPerson = { name: "Alice" }; // Geçerli

Readonly<T>

Readonly<T>, T türünün tüm özelliklerini salt okunur yapar:


interface Person {
  name: string;
  age: number;
}

type ReadonlyPerson = Readonly<Person>;

const readonlyPerson: ReadonlyPerson = { name: "Bob", age: 42 };
// readonlyPerson.age = 43; // Hata: 'age' salt okunur bir özellik olduğu için atama yapılamaz.

Pick<T, K>

Pick<T, K>, T türünden bir dizi K özelliği seçer:


interface Person {
  name: string;
  age: number;
  email: string;
}

type NameAndAge = Pick<Person, "name" | "age">;

const nameAndAge: NameAndAge = { name: "Charlie", age: 28 };

Omit<T, K>

Omit<T, K>, T türünden bir dizi K özelliğini kaldırır:


interface Person {
  name: string;
  age: number;
  email: string;
}

type PersonWithoutEmail = Omit<Person, "email">;

const personWithoutEmail: PersonWithoutEmail = { name: "David", age: 35 };

Record<K, T>

Record<K, T>, anahtarları K ve değerleri T türünde olan bir tür oluşturur:


type CountryCodes = "US" | "CA" | "UK" | "DE" | "FR" | "JP" | "CN" | "IN" | "BR" | "AU"; // Küresel bağlam için genişletilmiş liste
type Currency = "USD" | "CAD" | "GBP" | "EUR" | "JPY" | "CNY" | "INR" | "BRL" | "AUD"; // Küresel bağlam için genişletilmiş liste

type CurrencyMap = Record<CountryCodes, Currency>;

const currencyMap: CurrencyMap = {
  "US": "USD",
  "CA": "CAD",
  "UK": "GBP",
  "DE": "EUR",
  "FR": "EUR",
  "JP": "JPY",
  "CN": "CNY",
  "IN": "INR",
  "BR": "BRL",
  "AU": "AUD",
};

Eşlenmiş Tipler (Mapped Types)

Eşlenmiş tipler, mevcut türleri özelliklerini yineleyerek dönüştürmenize olanak tanır. Bu, mevcut olanlara dayalı yeni türler oluşturmanın güçlü bir yoludur. Örneğin, başka bir türün tüm özelliklerini salt okunur yapan bir tür oluşturabilirsiniz:


interface Person {
  name: string;
  age: number;
}

type ReadonlyPerson = {
  readonly [K in keyof Person]: Person[K];
};

const readonlyPerson: ReadonlyPerson = { name: "Eve", age: 25 };
// readonlyPerson.age = 26; // Hata: 'age' salt okunur bir özellik olduğu için atama yapılamaz.

Bu örnekte, [K in keyof Person], Person arayüzünün tüm anahtarları üzerinde yinelenir ve Person[K] her bir özelliğin türüne erişir. readonly anahtar kelimesi her özelliği salt okunur yapar.

Koşullu Tipler (Conditional Types)

Koşullu tipler, koşullara göre tipler tanımlamanıza olanak tanır. Bu, farklı senaryolara uyum sağlayan tipler oluşturmanın güçlü bir yoludur.


type NonNullable<T> = T extends null | undefined ? never : T;

type MaybeString = string | null | undefined;
type StringType = NonNullable<MaybeString>; // string

function getValue<T>(value: T): NonNullable<T> {
  if (value == null) { // Hem null hem de undefined durumlarını ele alır
    throw new Error("Değer null veya undefined olamaz");
  }
  return value as NonNullable<T>;
}

try {
  const validValue = getValue("hello");
  console.log(validValue.toUpperCase()); // Çıktı: HELLO

  const invalidValue = getValue(null); // Bu bir hata fırlatacaktır
  console.log(invalidValue); // Bu satıra ulaşılamayacak
} catch (error: any) {
  console.error(error.message); // Çıktı: Değer null veya undefined olamaz
}

Bu örnekte, NonNullable<T> türü, T'nin null veya undefined olup olmadığını kontrol eder. Eğer öyleyse, türün izin verilmediği anlamına gelen never döndürür. Aksi takdirde, T döndürür. Bu, null olamayacağı garanti edilen türler oluşturmanıza olanak tanır.

Jenerik Kullanımı için En İyi Uygulamalar

Jenerikleri kullanırken aklınızda bulundurmanız gereken bazı en iyi uygulamalar şunlardır:

Küresel Bağlamda Örnekler

Jeneriklerin küresel bir bağlamda nasıl kullanılabileceğine dair bazı örneklere bakalım:

Para Birimi Dönüşümü


interface ConversionRate {
  rate: number;
  fromCurrency: string;
  toCurrency: string;
}

function convertCurrency<T extends ConversionRate>(amount: number, rate: T): number {
  return amount * rate.rate;
}

const usdToEurRate: ConversionRate = { rate: 0.85, fromCurrency: "USD", toCurrency: "EUR" };
const amountInUSD = 100;
const amountInEUR = convertCurrency(amountInUSD, usdToEurRate);
console.log(`${amountInUSD} USD, ${amountInEUR} EUR'ya eşittir`); // Çıktı: 100 USD, 85 EUR'ya eşittir

Tarih Biçimlendirme


interface DateFormatOptions {
  locale: string;
  options: Intl.DateTimeFormatOptions;
}

function formatDate<T extends DateFormatOptions>(date: Date, format: T): string {
  return date.toLocaleDateString(format.locale, format.options);
}

const currentDate = new Date();

const usDateFormat: DateFormatOptions = { locale: "en-US", options: { year: 'numeric', month: 'long', day: 'numeric' } };
const germanDateFormat: DateFormatOptions = { locale: "de-DE", options: { year: 'numeric', month: 'long', day: 'numeric' } };
const japaneseDateFormat: DateFormatOptions = { locale: "ja-JP", options: { year: 'numeric', month: 'long', day: 'numeric' } };

console.log("ABD Tarihi: " + formatDate(currentDate, usDateFormat));
console.log("Alman Tarihi: " + formatDate(currentDate, germanDateFormat));
console.log("Japon Tarihi: " + formatDate(currentDate, japaneseDateFormat));

Çeviri Servisi


interface Translation {
  [key: string]: string; // Dinamik dil anahtarlarına izin verir
}

interface LanguageData<T extends Translation> {
  languageCode: string;
  translations: T;
}

const englishTranslations: Translation = {
  "hello": "Hello",
  "goodbye": "Goodbye",
  "welcome": "Welcome to our website!"
};

const spanishTranslations: Translation = {
  "hello": "Hola",
  "goodbye": "Adiós",
  "welcome": "¡Bienvenido a nuestro sitio web!"
};

const frenchTranslations: Translation = {
  "hello": "Bonjour",
  "goodbye": "Au revoir",
  "welcome": "Bienvenue sur notre site web !"
};


const languageData: LanguageData<typeof englishTranslations>[] = [
  {languageCode: "en", translations: englishTranslations },
  {languageCode: "es", translations: spanishTranslations },
  {languageCode: "fr", translations: frenchTranslations}
];

function translate<T extends Translation>(key: string, languageCode: string, languageData: LanguageData<T>[]): string {
  const lang = languageData.find(lang => lang.languageCode === languageCode);
  if (!lang) {
    return `${languageCode} dilinde ${key} için çeviri bulunamadı.`;
  }
  return lang.translations[key] || `${key} için çeviri bulunamadı.`;
}

console.log(translate("hello", "en", languageData)); // Çıktı: Hello
console.log(translate("hello", "es", languageData)); // Çıktı: Hola
console.log(translate("welcome", "fr", languageData)); // Çıktı: Bienvenue sur notre site web !
console.log(translate("missingKey", "de", languageData)); // Çıktı: de dilinde missingKey için çeviri bulunamadı.

Sonuç

TypeScript jenerikleri, karmaşık veri tipleriyle çalışabilen yeniden kullanılabilir, tür güvenli kod yazmak için güçlü bir araçtır. Jeneriklerin temel sözdizimini, gelişmiş özelliklerini ve en iyi uygulamalarını anlayarak, TypeScript uygulamalarınızın kalitesini ve sürdürülebilirliğini önemli ölçüde artırabilirsiniz. Küresel bir kitle için uygulama geliştirirken, jenerikler çeşitli veri formatlarını ve kültürel gelenekleri ele almanıza yardımcı olarak herkes için sorunsuz bir kullanıcı deneyimi sağlayabilir.